﻿#region Namespaces

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.XPath;

using Vibstudio.X4NET.Collections;
using Vibstudio.X4NET.Xml.Linq;
using Vibstudio.X4NET.Xml.Navigation;

#endregion

namespace Vibstudio.X4NET.Xml.XPath
{
    public static class XPathExtensions
    {
        #region Public operations

        public static string XPath(this XObject obj)
        {
            var builder = new StringBuilder();

            var namespacePrefixes = new Dictionary<string, string>();
            var curObject = obj;
            while (curObject != null)
            {
                var element = curObject as XElement;
                if (element != null)
                {
                    StoreNamespacePrefixed(element, namespacePrefixes);
                }
                curObject = curObject.Parent;
            }

            curObject = obj;

            while (curObject != null)
            {
                builder.Insert(0, GetXPathName(curObject, namespacePrefixes));
                builder.Insert(0, '/');
                curObject = curObject.Parent;
            }

            string xpath = builder.ToString();

            const string BRACKET_PATTERN = @"\[(\w)\]";
            Regex regex = new Regex(BRACKET_PATTERN);
            xpath = regex.Replace(xpath, string.Empty, 1);

            return xpath;
        }

        public static XPathNavigator Navigator(this XNode node)
        {
            return node.Navigator(new NameTable());
        }

        public static XPathNavigator Navigator(this XNode node, XmlNameTable nameTable)
        {
            return new XNodeNavigator(node, nameTable);
        }

        public static object Evaluate(this XNode node, string expression, IXmlNamespaceResolver resolver = null)
        {
            object navigationResult = node.Navigator().Evaluate(expression, resolver);
            if (!(navigationResult is XPathNodeIterator))
            {
                return navigationResult;
            }
            return GetUnderlyingXObjects((XPathNodeIterator)navigationResult);
        }

        private static IEnumerable<object> GetUnderlyingXObjects(XPathNodeIterator nodeIterator)
        {
            return from XPathNavigator navigator in nodeIterator select navigator.UnderlyingObject;
        }

        public static XElement XPathSelectElement(this XNode node, string expression, IXmlNamespaceResolver resolver = null)
        {
            XPathNavigator nav = node.Navigator().SelectSingleNode(expression, resolver);
            if (nav == null)
            {
                return null;
            }
            return nav.UnderlyingObject as XElement;
        }

        public static IEnumerable<XElement> XPathSelectElements(this XNode node, string expression, IXmlNamespaceResolver resolver = null)
        {
            XPathNodeIterator iter = node.Navigator().Select(expression, resolver);
            return (from XPathNavigator nav in iter select nav.UnderlyingObject).OfType<XElement>();
        }

        public static XPathList RemovePositionality(this IEnumerable<string> xpathList)
        {
            XPathList result = new XPathList();
            result.AddRange(xpathList.Select(s => s.RemovePositionality()));
            return result;
        }

        public static string RemovePositionality(this string xpath)
        {
            const string POSITIONALITY_PATTERN = @"\[(.*?)\]";
            return Regex.Replace(xpath, POSITIONALITY_PATTERN, string.Empty);
        }

        public static List<string> RemoveNodeTest(this IEnumerable<string> xpathList, XPathNodeTestKind nodeTestKind = XPathNodeTestKind.Text)
        {
            List<string> result = new List<string>();
            foreach (string s in xpathList)
            {
                result.Add(s.RemoveNodeTest(nodeTestKind));
            }
            return result;
        }

        public static string RemoveNodeTest(this string xpath, XPathNodeTestKind nodeTestKind = XPathNodeTestKind.Text)
        {
            return Regex.Replace(xpath, NodeTest.Remove.Pattern(nodeTestKind), string.Empty);
        }

        public static XPathList AddNodeTest(this IEnumerable<string> xpathList, XPathNodeTestKind nodeTestKind = XPathNodeTestKind.Text)
        {
            XPathList result = new XPathList();
            result.AddRange(xpathList.Select(s => s.AddNodeTest(nodeTestKind)));
            return result;
        }

        public static string AddNodeTest(this string xpath, XPathNodeTestKind nodeTestKind = XPathNodeTestKind.Text)
        {
            return !xpath.Contains(@"@") ? string.Format("{0}{1}", xpath, NodeTest.Add.Pattern(nodeTestKind)) : xpath;
        }

        public static List<string> Compact(this IEnumerable<string> xpathList)
        {
            List<string> result = new List<string>();
            foreach (string s in xpathList)
            {
                result.Add(s.Compact());
            }
            return result;
        }

        /// <summary>
        /// Compatta l'XPath rimuovendo eventuali rappresentazioni posizionali (vd. [1]) e/o testuali (es. /text()).
        /// </summary>
        /// <param name="xpath">L'XPath da compattare</param>
        /// <returns>L'XPath compattato</returns>
        public static string Compact(this string xpath)
        {
            return xpath.RemovePositionality()
                        .RemoveNodeTest(XPathNodeTestKind.Comment)
                        .RemoveNodeTest(XPathNodeTestKind.Node)
                        .RemoveNodeTest(XPathNodeTestKind.ProcessingInstruction)
                        .RemoveNodeTest();
        }

        public static bool IsMatch(this string xpath1, string xpath2, StringComparison comparisonType)
        {
            return xpath1.Compact().Equals(xpath2.Compact(), comparisonType);
        }

        public static bool IsMatch(this string xpath1, string xpath2)
        {
            return xpath1.Compact().Equals(xpath2.Compact());
        }

        public static bool IsMatch(this XPathList list1, XPathList list2)
        {
            return list1.Compact().CompareList(list2.Compact());
        }

        public static int Cardinality(this XElement element)
        {
            return element.ElementsBeforeSelf().Count(sib => sib.Name.Equals(element.Name)) + 1;
        }

        public static string TagName(this string xpath)
        {
            // Se l'ultimo frammento è un "Node Test" si richiamerà ricorsivamente il metodo passando l'xpath ricostruito senza di esso
            string[] NODE_TEST_LIST = { "comment()", "node()", "processing-instruction()", "text()" };

            if (string.IsNullOrEmpty(xpath))
            {
                throw new ArgumentNullException("xpath");
            }
            
            string[] fragments = xpath.Split('/');
            if (fragments.Length > 0)
            {
                string lastFragment = fragments[fragments.Length - 1];
                if (!string.IsNullOrEmpty(lastFragment))
                {
                    if (lastFragment.Length > 0)
                    {
                        if (!new Regex("[A-Za-z]").IsMatch(lastFragment.Substring(0, 1)))
                        {
                            return lastFragment.Remove(0, 1);
                        }
                    }

                    if (lastFragment.Length > 3)
                    {
                        if (new Regex(@"(\[([0-9]+)\])").IsMatch(lastFragment.Substring(lastFragment.Length - 3, 3)))
                        {
                            return lastFragment.Remove(lastFragment.Length - 3, 3);
                        }
                    }

                    if (NODE_TEST_LIST.Contains(lastFragment))
                    {
                        string[] newFragments = fragments.Reverse().Skip(1).Reverse().ToArray();
                        string newXPath = string.Join("/", newFragments);
                        // Ricorsione...
                        return TagName(newXPath);
                    }

                    return lastFragment;
                }
            }

            return string.Empty;
        }

        public static XPathList ExtractXPaths(this XDocument document, bool positional = true, bool textual = false, bool includingAttributes = true, string xpath = "")
        {
            XPathList xpaths = new XPathList();

            if (!string.IsNullOrEmpty(xpath) && !xpath.Equals(Defaults.XPathRoot))
            {
                try
                {
                    XmlKeyValuePairCollection valuePairCollection;
                    if (TryParseAttributes(document, xpath, out valuePairCollection))
                    {
                        xpaths.AddRange(valuePairCollection.Select(valuePair => valuePair.XPath));
                        CheckXPaths(document, positional, textual, ref xpaths);
                        return xpaths;
                    }

                    XElement element = document.XPathSelectElement(xpath);
                    document = element.ToXDocument();
                    ExtractXPaths(document.Root, ref xpaths, includingAttributes);
                    CheckXPaths(document, positional, textual, ref xpaths);
                    return xpaths;
                }
                catch (Exception exception)
                {
                    Trace.WriteLine(string.Format("Exception: {0}", exception), "XPathExtensions.ExtractXPaths");
                }
            }
            else
            {
                ExtractXPaths(document.Root, ref xpaths, includingAttributes);
                CheckXPaths(document, positional, textual, ref xpaths);
                return xpaths;
            }

            return xpaths;
        }

        public static bool IsSimpleValue(this XElement element)
        {
            return IsSimpleValue(element.ToString());
        }

        public static bool IsSimpleValue(this XPathNavigator navigator)
        {
            return IsSimpleValue(navigator.OuterXml);
        }

        public static XPathList StripDeclarations(this XPathList xpaths)
        {
            string rootName = xpaths.RootName;
            return new XPathList(xpaths.FindAll(IsXmlDeclaration)) { RootName = rootName };
        }

        #endregion
        
        #region Internal operations

        internal static string BuildXPathName(string preffix, string name, int cardinality = 0)
        {
            var builder = new StringBuilder();

            if (!string.IsNullOrEmpty(preffix))
            {
                builder.Append(preffix);
                builder.Append(':');
            }

            builder.Append(name);

            if (cardinality > 0)
            {
                builder.Append('[');
                builder.Append(cardinality);
                builder.Append(']');
            }
            return builder.ToString();
        }

        internal static XPathList StripDeclarations(this XPathList xpaths, Predicate<string> match)
        {
            string rootName = xpaths.RootName;
            return new XPathList(xpaths.FindAll(match)) { RootName = rootName };
        }

        internal static bool TryParseElements(this XDocument xml, string xpath, out XmlKeyValuePairCollection collection)
        {
            collection = new XmlKeyValuePairCollection();

            try
            {
                XDocument document = ((IEnumerable)xml.Evaluate(xpath)).Cast<XDocument>().FirstOrDefault();
                if (document != null)
                {
                    IEnumerable<XElement> elements = document.Elements();
                    collection.AddRange(elements.Select(element => new XmlKeyValuePair(element.Name.LocalName, element.Value, element.XPath())));
                }
                return true;
            }
            catch (Exception exception)
            {
                Trace.WriteLine("Exception: " + exception, "XPathExtensions.TryParseElement");
                return false;
            }
        }

        internal static bool TryParseElement(this XDocument document, string xpath, out XmlKeyValuePair valuePair)
        {
            valuePair = null;

            try
            {
                XElement element = ((IEnumerable)document.Evaluate(xpath)).Cast<XElement>().FirstOrDefault();
                if (element == null)
                {
                    return false;
                }
                valuePair = new XmlKeyValuePair(element.Name.LocalName, element.Value, xpath);
                return true;
            }
            catch (Exception exception)
            {
                Trace.WriteLine(string.Format("Exception: {0}", exception), "XPathExtensions.TryParseElement");
                return false;
            }
        }

        internal static bool TryParseAttributes(this XDocument document, string xpath, out XmlKeyValuePairCollection collection)
        {
            collection = new XmlKeyValuePairCollection();

            try
            {
                IEnumerable<XAttribute> attributes = ((IEnumerable)document.Evaluate(xpath)).Cast<XAttribute>();
                foreach (XAttribute attribute in attributes)
                {
                    if (attribute.Owner.GetType() == typeof(XElement))
                    {
                        XmlKeyValuePairCollection valuePairCollection;
                        if (TryParseElements(attribute.Owner.ToXDocument(), xpath, out valuePairCollection))
                        {
                            collection.AddRange(valuePairCollection);
                        }
                        else
                        {
                            collection.Add(new XmlKeyValuePair(attribute.Name.LocalName, attribute.Value, attribute.XPath()));
                        }
                    }
                    else
                    {
                        collection.Add(new XmlKeyValuePair(attribute.Name.LocalName, attribute.Value, attribute.XPath()));
                    }
                }
                return true;
            }
            catch (Exception exception)
            {
                Trace.WriteLine(string.Format("Exception: {0}", exception), "XPathExtensions.TryParseElement");
                return false;
            }
        }

        internal static bool TryParseAttribute(this XDocument document, string xpath, out XmlKeyValuePair valuePair)
        {
            valuePair = null;

            try
            {
                XAttribute attribute = ((IEnumerable)document.Evaluate(xpath)).Cast<XAttribute>().FirstOrDefault();
                if (attribute == null)
                {
                    return false;
                }
                valuePair = new XmlKeyValuePair(attribute.Name.LocalName, attribute.Value, xpath);
                return true;
            }
            catch (Exception exception)
            {
                Trace.WriteLine("Exception: " + exception, "XPathExtensions.TryParseElement");
                return false;
            }
        }

        #endregion

        #region Helpers

        private static void CheckXPaths(XDocument document, bool positional, bool textual, ref XPathList xpaths)
        {
            if (!positional)
            {
                xpaths = xpaths.RemovePositionality();
            }
            if (textual)
            {
                xpaths = xpaths.AddNodeTest();
            }
            xpaths.RootName = document.RootName();
        }

        private static string GetXPathName(XObject obj, IDictionary<string, string> namespacePrefixes)
        {
            IDictionary<Type, IObjectXPathName> xpathNames = new Dictionary<Type, IObjectXPathName>
                                                             {
                                                                 { typeof(XElement), new ElementXPathName() },
                                                                 { typeof(XAttribute), new AttributeXPathName() },
                                                                 { typeof(XText), new TextXPathName() },
                                                                 { typeof(XCData), new TextXPathName() },
                                                                 { typeof(XComment), new CommentXPathName() }
                                                             };

            return xpathNames[obj.GetType()].GetXPathName(obj, namespacePrefixes);
        }

        private static void StoreNamespacePrefixed(XElement element, IDictionary<string, string> namespacePrefixes)
        {
            var attributes = element.Attributes().Where(a => a.IsNamespaceDeclaration);

            foreach (var attribute in attributes)
            {
                var prefix = attribute.Name.NamespaceName == string.Empty ? string.Empty : attribute.Name.LocalName;
                namespacePrefixes.Add(attribute.Value, prefix);
            }
        }

        private static void ExtractXPaths(this XElement element, ref XPathList xpaths, bool includingAttributes = true)
        {
            if (xpaths == null)
            {
                xpaths = new XPathList();
            }

            if (includingAttributes)
            {
                if (element.HasAttributes)
                {
                    foreach (XAttribute attribute in element.Attributes())
                    {
                        ExtractXPaths(element, attribute, ref xpaths);
                    }
                }
            }

            if (!element.HasElements)
            {
                if (includingAttributes && element.HasAttributes)
                {
                    foreach (XAttribute attribute in element.Attributes())
                    {
                        ExtractXPaths(element, attribute, ref xpaths);
                    }
                }

                xpaths.Add(element.XPath());
            }
            else
            {
                foreach (XElement e in element.Elements())
                {
                    ExtractXPaths(e, ref xpaths, includingAttributes);
                }
            }
        }

        private static void ExtractXPaths(XElement element, XAttribute attribute, ref XPathList xpaths)
        {
            string elementXPath = element.XPath();
            string attributeXPath = string.Format("{0}/@{1}", elementXPath, attribute.Name.LocalName);

            if (!xpaths.Contains(elementXPath))
            {
                if (!IsRoot(element))
                {
                    if (IsSimpleValue(element.ToString()))
                    {
                        xpaths.Add(elementXPath);
                    }
                }

                if (!xpaths.Contains(attributeXPath))
                {
                    xpaths.Add(attributeXPath);
                }
            }
            else
            {
                if (!xpaths.Contains(attributeXPath))
                {
                    xpaths.Add(attributeXPath);
                }
            }
        }

        private static bool IsRoot(XElement element)
        {
            string[] xpathSegments = element.XPath().Split('/');
            int counter = xpathSegments.Count(segment => !string.IsNullOrEmpty(segment));
            return counter == 1;
        }

        private static bool IsSimpleValue(string xml)
        {
            bool isValue = true;
            int counterElements = 0;

            ASCIIEncoding encoding = new ASCIIEncoding();
            MemoryStream stream = new MemoryStream(encoding.GetBytes(xml));

            using (XmlReader reader = XmlReader.Create(stream))
            {
                while (reader.Read())
                {
                    // If current node has attributes is not a simple value!
                    if (reader.HasAttributes)
                    {
                        return false;
                    }

                    XmlNodeType nodeType = reader.MoveToContent();
                    if (nodeType == XmlNodeType.Element)
                    {
                        counterElements++;
                    }
                }
            }

            if (counterElements > 1)
            {
                isValue = false;
            }

            return isValue;
        }

        private static bool IsXmlDeclaration(string xpath)
        {
            string[] declarations = { "xmlns", "xmlns:xsi", "xsi:noNamespaceSchemaLocation" };

            return declarations.All(declaration => declaration.IndexOf(xpath.TagName(), StringComparison.CurrentCultureIgnoreCase) == -1);
        }

        #endregion

        #region Inner classes

        private class NodeTest
        {
            private static IPatternProvider addPatternProvider;

            public static IPatternProvider Add
            {
                get { return addPatternProvider ?? (addPatternProvider = new AddPatterProvider()); }
            }

            private static IPatternProvider removePatternProvider;

            public static IPatternProvider Remove
            {
                get { return removePatternProvider ?? (removePatternProvider = new RemovePatterProvider()); }
            }
        }

        private interface IPatternProvider
        {
            string Pattern(XPathNodeTestKind nodeTestKind);
        }

        private class AddPatterProvider : IPatternProvider
        {
            public string Pattern(XPathNodeTestKind nodeTestKind)
            {
                switch (nodeTestKind)
                {
                    case XPathNodeTestKind.Comment:
                        return @"/comment()";
                    case XPathNodeTestKind.Node:
                        return @"/node()";
                    case XPathNodeTestKind.ProcessingInstruction:
                        return @"/processing-instruction()";
                    default:
                        return @"/text()";
                }
            }
        }

        private class RemovePatterProvider : IPatternProvider
        {
            public string Pattern(XPathNodeTestKind nodeTestKind)
            {
                switch (nodeTestKind)
                {
                    case XPathNodeTestKind.Comment:
                        return @"/comment\(\)";
                    case XPathNodeTestKind.Node:
                        return @"/node\(\)";
                    case XPathNodeTestKind.ProcessingInstruction:
                        return @"/processing-instruction\(\)";
                    default:
                        return @"/text\(\)";
                }
            }
        }

        #endregion
    }
}